package com.googlecode.nls4j;

import java.util.Locale;

//@formatter:off
/**
 * A variant of {@link CurrentBundle} which listens a {@link ThreadLocal} copy 
 * of the {@link NlsContext} and stores the reference to the current bundle in 
 * either {@link ThreadLocal} or {@link InheritableThreadLocal} variable.
 * <br/><br/>
 * With caution this implementation may be used in web applications. Since most
 * servlet containers handle incoming HTTP requests in a separate thread, we can
 * implement a <a href=
 * "http://download.oracle.com/javaee/6/api/javax/servlet/ServletRequestListener.html"
 * >ServletRequestListener</a> which will configure the ThreadLocalBundle in the
 * beginning of the request handling. Here is an example of such use case:
 * 
 * <pre>
 * public enum Msg implements NlsKey&lt;String&gt; {
 *     Hello(&quot;Hello&quot;),
 *     Bye(&quot;Bye&quot;);
 * 
 *     private static final CurrentBundle&lt;Msg, String&gt; localBundle = ThreadLocalBundle
 *             .newInstance(
 *                 new BundleStorage&lt;Msg, String&gt;(Msg.class, EnumBundleLoader
 *                         .&lt;Msg, String&gt; getDefault(false)), true);
 * 
 *     private final String defaultValue;
 * 
 *     private Msg(final String defaultValue) {
 *         this.defaultValue = defaultValue;
 *     }
 * 
 *     public static CurrentBundle&lt;Msg, String&gt; getCurrentBundle() {
 *         return localBundle;
 *     }
 * 
 *     public String getName() {
 *         return name();
 *     }
 * 
 *     public String getDefaultValue(final Locale locale) {
 *         return defaultValue;
 *     }
 * 
 *     public String toString() {
 *         return localBundle.get(this);
 *     }
 * }
 * 
 * public class AppRequestListener implements ServletRequestListener {
 *     private static final Locale DEFAULT_LANG = new Locale("en");
 * 
 *     public void requestInitialized(final ServletRequestEvent e) {
 *         final HttpServletRequest req = (HttpServletRequest) e.getServletRequest();
 *         final Locale lang = (Locale) req.getSession().getAttribute(&quot;lang&quot;);
 * 
 *         if (lang != null) {
 *             Msg.getCurrentBundle().getNlsContext().setLocale(lang);
 *         } else {
 *             Msg.getCurrentBundle().getNlsContext().setLocale(DEFAULT_LANG);
 *         }
 *     }
 * 
 *     public void requestDestroyed(final ServletRequestEvent e) {
 *     }
 * }
 * </pre>
 * 
 * @author Andrey Pavlenko
 */
//@formatter:on
public abstract class ThreadLocalBundle<K extends NlsKey<V>, V> extends
        CurrentBundle<K, V> {
    
    /**
     * Returns true if this implementation uses {@link InheritableThreadLocal},
     * returns false if this implementation uses {@link ThreadLocal} variable to
     * store reference to the current bundle.
     */
    public abstract boolean isInheritable();

    /**
     * Returns {@linkplain #newInstance(BundleStorage, boolean, boolean)
     * newInstance(bundleStorage, lazy, false)}.
     */
    public static <K extends NlsKey<V>, V> ThreadLocalBundle<K, V> newInstance(
            final BundleStorage<K, V> bundleStorage, final boolean lazy) {
        return newInstance(bundleStorage, lazy, false);
    }

    /**
     * Returns
     * {@linkplain #newInstance(BundleStorage, boolean, boolean, ThreadLocal)
     * newInstance(bundleStorage, lazy, inheritable, null)}.
     */
    public static <K extends NlsKey<V>, V> ThreadLocalBundle<K, V> newInstance(
            final BundleStorage<K, V> bundleStorage, final boolean lazy,
            final boolean inheritable) {
        return newInstance(bundleStorage, lazy, inheritable, null);
    }

    /**
     * Creates new {@linkplain ThreadLocalBundle} with the specified parameters.
     * If the argument {@code localContext} is null, either
     * {@link NlsContext#getThreadLocal()} (if {@code inheritable} == false) or
     * {@link NlsContext#getInheritableThreadLocal()} (if {@code inheritable} ==
     * true) will be used.
     * 
     * @throws NullPointerException
     *             if {@code bundleStorage} is null.
     */
    public static <K extends NlsKey<V>, V> ThreadLocalBundle<K, V> newInstance(
            final BundleStorage<K, V> bundleStorage, final boolean lazy,
            final boolean inheritable, ThreadLocal<NlsContext> localContext)
            throws NullPointerException {
        if (bundleStorage == null) {
            throw new NullPointerException();
        }
        if (localContext == null) {
            localContext = inheritable ? InheritableThreadLocalContext.instance
                    : ThreadLocalContext.instance;
        }
        return lazy ? new LazyBundleImpl<K, V>(
                bundleStorage, inheritable, localContext)
                : new NonLazyBundleImpl<K, V>(
                        bundleStorage, inheritable, localContext);
    }

    private abstract static class AbstractThreadLocalBundle<K extends NlsKey<V>, V>
            extends ThreadLocalBundle<K, V> {
        final Object handback = new Object();
        final boolean inheritable;
        final BundleStorage<K, V> bundleStorage;
        final ThreadLocal<NlsContext> localContext;

        AbstractThreadLocalBundle(final BundleStorage<K, V> bundleStorage,
                final ThreadLocal<NlsContext> localContext,
                final boolean inheritable) {
            this.inheritable = inheritable;
            this.bundleStorage = bundleStorage;
            this.localContext = localContext;
        }

        @Override
        public final NlsContext getNlsContext() {
            return localContext.get();
        }

        @Override
        public final BundleStorage<K, V> getBundleStorage() {
            return bundleStorage;
        }

        @Override
        public final boolean isInheritable() {
            return inheritable;
        }
    }

    private static final class LazyBundleImpl<K extends NlsKey<V>, V> extends
            AbstractThreadLocalBundle<K, V> {
        private final ThreadLocal<NlsBundle<K, V>> localBundle;
        private final ThreadLocal<Boolean> localRegistered;

        public LazyBundleImpl(final BundleStorage<K, V> bundleStorage,
                final boolean inheritable,
                final ThreadLocal<NlsContext> localContext) {
            super(bundleStorage, localContext, inheritable);

            if (inheritable) {
                localRegistered = new InheritableThreadLocal<Boolean>();
                localBundle = new InheritableThreadLocal<NlsBundle<K, V>>();
            } else {
                localRegistered = new ThreadLocal<Boolean>();
                localBundle = new ThreadLocal<NlsBundle<K, V>>();
            }
        }

        @Override
        public NlsBundle<K, V> getBundle() {
            NlsBundle<K, V> s = localBundle.get();

            if (s == null) {
                final NlsContext ctx = localContext.get();

                if (localRegistered.get() == null) {
                    ctx.addLocaleChangeListener(this, handback);
                    localRegistered.set(true);
                }

                s = bundleStorage.getBundle(ctx.getLocale());
                localBundle.set(s);
            }

            return s;
        }

        @Override
        public void handleLocaleChange(final NlsContext context,
                final Locale oldLocale, final Locale newLocale,
                final Object handback) {
            if (handback == this.handback) {
                localBundle.set(null);
            }
        }

        @Override
        public boolean isLazy() {
            return true;
        }
    }

    private static final class NonLazyBundleImpl<K extends NlsKey<V>, V>
            extends AbstractThreadLocalBundle<K, V> {
        private final ThreadLocal<NlsBundle<K, V>> localBundle;

        public NonLazyBundleImpl(final BundleStorage<K, V> bundleStorage,
                final boolean inheritable,
                final ThreadLocal<NlsContext> localContext) {
            super(bundleStorage, localContext, inheritable);

            if (inheritable) {
                localBundle = new InheritableThreadLocal<NlsBundle<K, V>>() {
                    @Override
                    protected NlsBundle<K, V> initialValue() {
                        final NlsContext ctx = localContext.get();
                        ctx.addLocaleChangeListener(NonLazyBundleImpl.this,
                            handback);
                        return bundleStorage.getBundle(ctx.getLocale());
                    }
                };
            } else {
                localBundle = new ThreadLocal<NlsBundle<K, V>>() {
                    @Override
                    protected NlsBundle<K, V> initialValue() {
                        final NlsContext ctx = localContext.get();
                        ctx.addLocaleChangeListener(NonLazyBundleImpl.this,
                            handback);
                        return bundleStorage.getBundle(ctx.getLocale());
                    }
                };
            }
            localBundle.get();
        }

        @Override
        public NlsBundle<K, V> getBundle() {
            return localBundle.get();
        }

        @Override
        public void handleLocaleChange(final NlsContext context,
                final Locale oldLocale, final Locale newLocale,
                final Object handback) {
            if (handback == this.handback) {
                localBundle.set(bundleStorage.getBundle(newLocale));
            }
        }

        @Override
        public boolean isLazy() {
            return false;
        }
    }

    private static final class ThreadLocalContext extends
            ThreadLocal<NlsContext> {
        static final ThreadLocalContext instance = new ThreadLocalContext();

        @Override
        protected NlsContext initialValue() {
            return NlsContext.getThreadLocal();
        }
    }

    private static final class InheritableThreadLocalContext extends
            InheritableThreadLocal<NlsContext> {
        static final InheritableThreadLocalContext instance = new InheritableThreadLocalContext();

        @Override
        protected NlsContext initialValue() {
            return NlsContext.getInheritableThreadLocal();
        }
    }
}
